package org.hugoduncan.appam.component;

import java.io.IOException;
import java.io.StringWriter;
import java.net.URISyntaxException;
import java.util.Date;
import java.util.UUID;

import javax.activation.MimeType;
import javax.activation.MimeTypeParseException;
import javax.servlet.ServletException;

import org.apache.abdera.model.Content;
import org.apache.abdera.model.Document;
import org.apache.abdera.model.Element;
import org.apache.abdera.model.Entry;
import org.apache.abdera.model.IRI;
import org.apache.abdera.model.Link;
import org.apache.abdera.model.Text.Type;
import org.apache.abdera.parser.ParseException;
import org.apache.abdera.parser.Parser;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import org.hugoduncan.appam.model.Constants;
import org.hugoduncan.appam.model.EntryComponent;
import org.hugoduncan.appam.server.AppServletRequest;
import org.hugoduncan.appam.server.AppServletResponse;

/**
 * Base class for an Entry.
 *
 * @author duncanhugo
 *
 */
public abstract class BaseEntryComponent
    implements EntryComponent
{
    private String location="/edit";

    protected static final Log log = LogFactory.getLog(FileSystemEntry.class);

    public String getLocation() {
        return location;
    }

    public void setLocation(String location) {
        this.location = location;
    }

    /**
     * Return a context object that will be used in all functions handling a 
     * request.
     * @return
     */
    protected AppRequestContext createRequestContext()
    {
        return null;
    }
    
    /**
     * Process an APP request.  This implementation checks the path for a
     * match to the location and dispatched to the getResource, deleletResource,
     * createResource and updateResource methods.
     */
    public Entry process(AppServletRequest request, AppServletResponse response)
        throws ServletException, IOException
    {

        String path=request.getPathInfo();
        AppRequestContext context=createRequestContext();

        if (request.getMethod().equals("POST") || path.startsWith(location))
        {

            if (request.getMethod().equals("GET"))
            {
                return getResource(request, response, context);
            }
            else if (request.getMethod().equals("PUT"))
            {
                return updateResource(request, response, context);
            }
            else if (request.getMethod().equals("DELETE"))
            {
                return deleteResource(request, response, context);
            }
            else if (request.getMethod().equals("POST"))
            {
                return createResource(request, response, context);
            }
        }
        return null;
    }

    /**
     * Process a GET request for a resource
     * @param request
     * @param response
     * @param context TODO
     * @return
     * @throws ServletException
     * @throws IOException
     */
    protected Entry getResource(AppServletRequest request, AppServletResponse response, AppRequestContext context)
        throws ServletException, IOException
    {
        Entry existingEntry=retrieveExistingEntry(request, context);
        if (existingEntry==null)
        {
            response.setStatus(AppServletResponse.SC_NOT_FOUND);
            return null;
        }

        response.setStatus(AppServletResponse.SC_OK);
        sendEntryResponse(response, existingEntry);
        return existingEntry;
    }

    protected Entry deleteResource(AppServletRequest request, AppServletResponse response, AppRequestContext context) throws ServletException, IOException {
        Entry entry = deleteExistingResource(request, context);
        if (entry==null) {
            response.sendError(
                    AppServletResponse.SC_NOT_FOUND,
                    "Requested DELETE on non-existant resource");
        }
        return entry;
    }

    @SuppressWarnings("unchecked")
    protected Entry updateResource(
        AppServletRequest request, AppServletResponse response, AppRequestContext context)
        throws ServletException, IOException {

        Entry entry=null;

        if (request.isAtomContent())
        {
            Entry existingEntry=retrieveExistingEntry(request, context);
            if (existingEntry==null)
            {
                response.sendError(AppServletResponse.SC_NOT_FOUND,
                                   "The resource to update was not found");
                return null;
            }

            Entry newEntry=getRequestEntry(request, context);
            if (newEntry==null)
            {
                response.sendError(AppServletResponse.SC_BAD_REQUEST,
                                   "Invalid atom entry");
                return null;
            }

            updateEntry(existingEntry, newEntry);

            storeEntry(newEntry, context);
            entry=newEntry;

            response.setStatus(AppServletResponse.SC_OK);
        }
        else
        {
            Entry existingEntry=retrieveExistingMediaLinkEntry(request, context);
            if (existingEntry==null)
            {
                response.sendError(AppServletResponse.SC_NOT_FOUND,
                                   "The resource to update was not found");
                return null;
            }

            String url=storeMediaResource(request, context);
            updateMediaLinkEntry(existingEntry, url, request.getContentType());

            // update the updated time
            long requestDate=request.getDateHeader("Date");
            if (requestDate>0)
                existingEntry.setUpdated(new Date(requestDate));
            else
                existingEntry.setUpdated(new Date());

            storeEntry(existingEntry, context);
            entry=existingEntry;

            response.setStatus(AppServletResponse.SC_OK);

        }


    return entry;
    }

    /**
     * Process a POST request to create an entry
     * @param context TODO
     */
    protected Entry createResource(
        AppServletRequest request,
        AppServletResponse response, AppRequestContext context) throws ServletException, IOException {

        log.debug("Creating entry " +request.getMethod()+
                  " request to "+request.getPathInfo());
        Entry entry=null;
        {
            // create resource
            if (request.isAtomContent())
            {
                log.debug("Processing POST of atom entry");

                entry=getRequestEntry(request, context);
                if (entry==null)
                {
                    response.sendError(AppServletResponse.SC_BAD_REQUEST,
                        "Could not parse atom request");
                    return null;
                }

                log.debug("Received document");
                debugWrite(entry.getDocument());

                fillValidAtomEntry(entry);
                storeEntry(entry, context);

                response.setStatus(AppServletResponse.SC_CREATED);
                sendEntryResponse(response, entry);
            }
            else
            {
                log.debug("Processing POST of entry with non atom content");

                // see spec:  8.4 Media Resources and Media Link Entries

                // create the media resource with non-atom content, together
                // with the media link entry
                entry = createMediaLinkEntry(request, context);
                fillValidAtomEntry(entry);

                String url=storeMediaResource(request, context);
                updateMediaLinkEntry(entry, url, request.getContentType());

                storeEntry(entry, context);

                response.setStatus(AppServletResponse.SC_CREATED);
                sendEntryResponse(response, entry);
            }
        }
        return entry;
    }

    /**
     * Store entry, updating the edit link of the entry to
     * reflect its location.
     * 
     * @param entry Atom Entry to be stored
     * @param context Request Context
     * @throws IOException
     */
    protected abstract void storeEntry(Entry entry, AppRequestContext context) throws IOException;

    /**
     * Create a Media link entry for a new Media Resource.
     * This method should not consume the request body, and is not responsable
     * for storing the resource.
     * @param request
     * @param context TODO
     * @return new media link entry
     */
    protected abstract Entry createMediaLinkEntry(AppServletRequest request, AppRequestContext context);

    /**
     * Retrieve the media link entry for an existing resource
     * @param request
     * @param context TODO
     * @return
     * @throws IOException
     */
     protected abstract Entry retrieveExistingMediaLinkEntry(AppServletRequest request, AppRequestContext context) throws IOException;

    /**
     * Retrieve the entry for an existing resource
     * @param request
     * @param context TODO
     * @return
     * @throws IOException
     */
    protected abstract Entry retrieveExistingEntry(AppServletRequest request, AppRequestContext context) throws IOException;

    /**
     * Store the media resource contents, returning its url.
     * @param mediaLinkEntry atom entry for the media link entry
     * @param context TODO
     * @param mediaResource  temporary file containing the resource
     * @param contentType media resource content type
     * @return URL of media resource, either relative to the location, or absolute
     * @throws IOException
     */
    protected abstract String storeMediaResource(AppServletRequest request, AppRequestContext context) throws IOException;


    /**
     * Delete the resource refenced in the request
     * @param request
     * @param context TODO
     * @return
     * @throws IOException
     */
    protected abstract Entry deleteExistingResource(AppServletRequest request, AppRequestContext context) throws IOException;

    
    /**
     * Obtain the Entry referenced in the request.
     * @param request
     * @param context TODO
     * @return entry
     */
    protected Entry getRequestEntry(AppServletRequest request, AppRequestContext context)
    {
        try {
            Document<Entry> document =
                Parser.INSTANCE.parse(request.getReader());
            return document.getRoot();
        }
        catch (ParseException e)
        {
            log.error("Could not read request body as Entry",e);
        } catch (IOException e) {
            log.error("Could not read request body as Entry",e);
        }
        return null;
    }

    /**
     * Update a media link entry to set the media resource url and content type
     * @param mediaLinkEntry
     * @param mediaResourceUrl
     * @param contentType
     */
    protected void updateMediaLinkEntry(Entry mediaLinkEntry, String mediaResourceUrl, String contentType) {
        Content content=null;
        if (contentType!=null)
        {
            MimeType mimeType=null;
            try {
                mimeType = new MimeType(contentType);
            } catch (MimeTypeParseException e1) {
                log.error("Couldnt process contentType : ", e1);
            }
            content=mediaLinkEntry.getFactory().newContent(mimeType);
        }
        else
            content=mediaLinkEntry.getFactory().newContent();

        try {
            content.setSrc(mediaResourceUrl);
        } catch (URISyntaxException e1) {
            log.error("Couldn't set edit-media src",e1);
        }
        mediaLinkEntry.setContentElement(content);
        mediaLinkEntry.setUpdated(new Date());

        try {
            mediaLinkEntry.addLink(mediaResourceUrl, "edit-media");
        } catch (URISyntaxException e) {
            log.error("Could not set media link entry's edit-media link", e);
        }
    }

    /**
     * If a resource has no id, this method is used to generate one.
     * By default this is a urn:uuid: based id.
     */
    protected String generateId() {
        return "urn:uuid:"+UUID.randomUUID().toString();
    }

    /**
     * Write an entry document as the servlet response
     * @param response
     * @param entry
     * @throws IOException
     */
    protected void sendEntryResponse(AppServletResponse response, Entry entry)
        throws IOException
    {
        // set headers specific to creation of the document
        try {
            String location = entry.getLink("edit").getHref().toString();
            if (location.startsWith("/"))
                location=response.getBaseUrl()+location;
            response.setHeader("Location", location);
        } catch (URISyntaxException e) {
            log.error("Entry with invalid edit link");
        }

        response.setContentType(Constants.SC_ATOM_CONTENT_TYPE);
        response.setCharacterEncoding("utf-8");

        // Write the response
        entry.getDocument().writeTo(response.getWriter());
    }

    /**
     * Update the new entry to reflect information from the old entry
     * @param existingEntry
     * @param newEntry
     */
    protected void updateEntry(Entry existingEntry, Entry newEntry) {
        // TODO: consistency checks and update from old to new
        newEntry.setUpdated(new Date());
        Link editLink=existingEntry.getLink("edit");
        if (newEntry.getLink("edit")!=null)
            newEntry.getLink("edit").discard();
        try {
            newEntry.addLink(editLink.getHref().toString(),editLink.getRel());
        } catch (URISyntaxException e) {
            log.error("Couldn't set edit link for updated document",e);
        }
        newEntry.setIdElement((IRI) existingEntry.getIdElement().clone());
    }

    /**
     * For a newly created atom entry, fill in any missing entries to make it valid
     * @param entry
     * @throws IOException
     */
    private void fillValidAtomEntry(Entry entry) throws IOException {
        // TODO: modify entry to ensure it conforms
        Date date=new Date();
        if (entry.getPublished()==null)
            entry.setPublished(date);
        if (entry.getUpdated()==null)
            entry.setUpdated(date);
        if (entry.getSummary()==null)
            entry.setSummary("",Type.TEXT);

        try {
            if (entry.getId()==null)
                entry.setId(generateId());
        } catch (URISyntaxException e) {
            log.error("Problem with entry ID",e);
        }
    }


    private <T extends Element> void debugWrite(Document<T> document)
    {
        try {
            StringWriter sw=new StringWriter();
            document.writeTo(sw);
            log.debug(sw.toString());
        } catch (IOException e1) {
            log.error("Could not output document",e1);
        }
    }

}